﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Elderos.Highlight.Search;
using Elderos.Ontology;
using Elderos.Utils.Extensions;
using Elderos.Utils.Logging;

namespace Elderos.Highlight.Rules
{
    [RankingRule("Population", RuleType.SelfInfo)]
    public class RulePopulation : RankingRule
    {
        public RulePopulation(IOntologyData ontology) : base(ontology) {}

        protected async override Task<Dictionary<TextEntity, IRuleResult>> InnerRank(HighlightResult highlightResult)
        {
            var results = new Dictionary<TextEntity, IRuleResult>();
            var entityPopulations = new Dictionary<int, long>();
            int attid = Ontology.Attributes.First(x => x.Name == "/location/statistical_region/population").ID;
            int[] entityids = highlightResult.Positions.SelectMany(x => x.EntitiesFlattened)
                                               .Select(x => x.EntityID)
                                               .Distinct()
                                               .ToArray();

            var attRefs = await Ontology.GetAttRefsAsync(new[] {attid}, entityids);

            //соответствие между сущностью и Dated integer
            IEnumerable<IGrouping<int, int>> populationCorrespondense = attRefs
                .GroupBy(x => x.EntityID, y => y.ReferenceID)
                .ToArray();

            int[] datedIntegersIDs = populationCorrespondense.SelectMany(x => x).ToArray();

            int dateAttID = Ontology.Attributes.First(x => x.Name == "/measurement_unit/dated_integer/year").ID;
            int numberAttID = Ontology.Attributes.First(x => x.Name == "/measurement_unit/dated_integer/number").ID;

            var attValues = await Ontology.GetAttValuesAsync(new[] {dateAttID, numberAttID}, datedIntegersIDs);

            var datedIntegersAttValues = attValues
                .GroupBy(x => x.EntityID)
                .ToDictionary(x => x.Key, x => x.Select(y => y));

            //grouping key - entityID
            //grouping elements - datedIntegerIDs
            foreach (var population in populationCorrespondense)
            {
                int entityID = population.Key;
                DatedNumber lastPopulation = GetLastPopulation(population, datedIntegersAttValues, numberAttID,
                                                                dateAttID);
                if (lastPopulation != null)
                    entityPopulations.Add(entityID, lastPopulation.Number);
            }

            foreach (TextEntity textEntity in highlightResult.Positions.SelectMany(x => x.EntitiesFlattened))
            {
                if (!entityPopulations.ContainsKey(textEntity.EntityID)) 
                    continue;
                if (entityPopulations[textEntity.EntityID] < Math.E)
                    continue;

                double population = entityPopulations[textEntity.EntityID];
                if (population <= 0) continue;
                double coef = Math.Log10(population);

                results.Add(textEntity, new ContiniousResult(Name, coef));
            }

            return results;
        }

        public override IRuleResult EmptyResult
        {
            get { return new ContiniousResult(Name, 0); }
        }

        private DatedNumber GetLastPopulation(IEnumerable<int> population, 
            IDictionary<int, IEnumerable<AttValue>> datedIntegersAttValues, int numberAttID, int dateAttID) 
        {
            var entityPopulations = new List<DatedNumber>();
            foreach (int dateIntegerID in population)
            {
                AttValue numberAttValue =
                    datedIntegersAttValues[dateIntegerID].FirstOrDefault(x => x.AttID == numberAttID);
                if (numberAttValue == default(AttValue))
                    continue;
                long number;
                if (!long.TryParse(numberAttValue.Value, out number))
                {
                    Logger.Error("Invalid attribute value(ID {0}): {1}", numberAttValue.ID, numberAttValue.Value);
                    continue;
                }

                AttValue dateAttValue =
                    datedIntegersAttValues[dateIntegerID].FirstOrDefault(x => x.AttID == dateAttID);
                if (dateAttValue != default(AttValue))
                {
                    Date date;
                    if (!Date.TryParse(dateAttValue.Value, out date))
                    {
                        Logger.Error("Invalid attribute value(ID {0}): {1}", dateAttValue.ID, dateAttValue.Value);
                        entityPopulations.Add(new DatedNumber{Date = null, Number = number});
                    }
                    else
                        entityPopulations.Add(new DatedNumber{Date = date, Number = number});
                }
            }

            return entityPopulations.OrderBy(x => x.Date).LastOrDefault();
        }

        private class DatedNumber : IComparable<DatedNumber>
        {
            public Date? Date { get; set; }
            public long Number { get; set; }

            #region Implementation of IComparable<in DatedNumber>

            /// <summary>
            /// Сравнивает текущий объект с другим объектом того же типа.
            /// </summary>
            /// <returns>
            /// Значение, указывающее, каков относительный порядок сравниваемых объектов.Расшифровка возвращенных значений приведена ниже.Значение Описание Меньше нуля Значение этого объекта меньше значения параметра <paramref name="other"/>.Нуль Значение этого объекта равно значению параметра <paramref name="other"/>. Больше нуля. Значение этого объекта больше значения параметра <paramref name="other"/>. 
            /// </returns>
            /// <param name="other">Объект, который требуется сравнить с данным объектом.</param>
            public int CompareTo(DatedNumber other)
            {
                if (!this.Date.HasValue && !other.Date.HasValue)
                    return 0;
                if (!other.Date.HasValue)
                    return 1;
                if (!this.Date.HasValue)
                    return -1;
                return this.Date.Value.CompareTo(other.Date.Value);
            }

            #endregion
        }
    }
}
